[UE]CharacterMovement源码浅析
CharacterMovement源码浅析
Base
classDiagram direction LR class UMovementComponent { UpdatedComponent : TObjectPtr~USceneComponent~ (处理空间位置) UpdatedPrimitive : TObjectPtr~UPrimitiveComponent~ (处理渲染物理) } %% ------------- UMovementComponent<|--UProjectileMovementComponent class UProjectileMovementComponent { 支持发射体(子弹等) } %% ------------- UMovementComponent<|--UNavMovementComponent class UNavMovementComponent { 支持 Agent 寻路 NavAgentProps : FNavAgentProperties } UNavMovementComponent<|--UPawnMovementComponent class UPawnMovementComponent { 支持输入控制 AddInputVector() } UPawnMovementComponent<|--UCharacterMovementComponent
Move
一般是先进行基础运动(PerformMovent
),然后处理基于物理的模拟(Collision
、Simulation
);
flowchart LR UCharacterMovementComponent TickComponent -->ConsumeInputVector TickComponent -->ControlledCharacterMove TickComponent -->Other...
ComsumeInputVector
:
从 PawnOwner
取出累积的 ControlInputVector
,该值监听输入并调用 Pawn::AddMovementInput
得来;
ControlledCharacterMove
:
进行 Character
移动的输入处理、物理模拟、同步;
Other...
ControlledCharacterMove
1 | void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds) |
flowchart LR ControlledCharacterMove -->CheckJumpInput -.->ScaleInputAcceleration -.->ComputeAnalogInputModifier ControlledCharacterMove -->|ROLE_Authority|PerformMovement -->Start(StartNewPhysics) ControlledCharacterMove -->|ROLE_AutonomousProxy & IsClient|ReplicateMoveToServer Start-->MOVE_None Start-->MOVE_Walking Start-->MOVE_Falling Start-->MOVE_Flying Start-->MOVE_Swimming Start-->MOVE_Custom
Input
解析输入相关的数据;
CheckJumpInput
:根据bPressedJump
,计算JumpCurrentCount
、JumpForceTimeRemaining
;ScaleInputAcceleration
:根据玩家的输入InputVector
,计算出当前的初始加速度值;ComputeAnalogInputModifier
:模拟输入修正值,将Acceleration /= MaxAcceleration
,限制在0-1
内;
PerfomeMovement
进行基础运动模拟,设置位移。
Kinetic : Walking
flowchart LR UCharacterMovementComponent %%------------------------------------- PerformMovement -->StartNewPhysics -->PhysWalking %%------------------------------------- PhysWalking -->GetSimulationTimeStep PhysWalking -->CalcVelocity PhysWalking -->MoveAlongFloor PhysWalking -->FindFloor PhysWalking -->CheckLedges PhysWalking -->MaintainHorizontalGroundVelocity %%------------------------------------- MoveAlongFloor -->ComputeGroundMovementDelta MoveAlongFloor -->SafeMoveUpdatedComponent %%------------------------------------- CheckLedges -->|true|GetLedgeMove GetLedgeMove -->|true|RevertMove -->TryLedgeMove GetLedgeMove -->|false|bMustJump -->|true|RevertMove -->Fall CheckLedges -->|false|FloorCheck %%------------------------------------- FloorCheck -->IsWalkableFloor IsWalkableFloor -->|true|AdjustFloorHeight -->SetBase IsWalkableFloor -->|false|GetPenetrationAdjustment -->ResolvePenetration %%-------------------------------------
GetSimulationTimeStep
:将 TickDeltaTime
按照 MaxSimulationTimeStep
分割为若干段(为了保证平滑),处理每一段的信息;
CalcVelocity
:根据 Friction
、bFluid
、BrakingDeceleration
修改 Acceleration
,并计算出 Velocity
水平速度;
MoveAlongFloor
:根据 MoveVelocity
信息,先调用 ComputeGroundMovementDelta
,根据 Velocity
计算出 RampVector
平行于斜面的移动距离,同时根据 bMaintainHorizontalGroundVelocity
处理沿斜面速度减慢的情况;然后调用 SafeMoveUpdatedComponent
,进行 MoveUpdatedComponent
,更新位置;在 UPrimitiveComponent::MoveComponentImpl
中,还会进行 World->ComponentSweepMulti
判断是否遇到障碍物;
FindFloor
:更新 CurrentFloor : FFindFloorResult
信息;用 CharacterOwner->GetCapsuleComponent()
进行 FloorSweepTest
,计算出 ValidPerchRadius
等信息;
CheckLedges
:检测是否在 Ledge
附近,如果是,则先尝试寻找新的移动方向(通过 GetLedgeMove
进行 SweepSingleByChannel
计算出边缘法线返回新的反向);如果找不到新的方向,则尝试跳跃,检测 bMustJump
,如果不能跳跃则取消移动;
FloorCheck
:校验 Floor
相关数据,如果 Character
处于IsWalkableFloor
的 Floor
,AdjustFloorHeight
来调整 Character
的高度,如果在 Floor
中则 GetPenetrationAdjustment
计算需要弹出 Character
的距离并 ResolvePenetration
,防止 Floor
和 Character
有冲突卡住;
MaintainHorizontalGroundVelocity
:调用之前判断是否依然 IsMovingOnGround
,如果是则根据 bMaintainHorizontalGroundVelocity
,计算 GravityRelativeVelocity
进而更新 Velocity
;
Kinetic : Falling
flowchart LR PerformMovement -->StartNewPhysics -->PhysFalling PhysFalling -->GetFallingLateralAcceleration PhysFalling -->ShouldLimitAirControl PhysFalling -->RestorePreAdditiveRootMotionVelocity PhysFalling -->CalcVelocity PhysFalling -->ApplyRootMotionToVelocity PhysFalling -->NotifyJumpApex PhysFalling -->SafeMoveUpdatedComponent PhysFalling -->|IsSwimming|StartSwimming PhysFalling -->BlockingHit %%--------------------- BlockingHit -->|IsValidLandingPoint|ProcessLanded BlockingHit -->HandleImpact %%--------------------- ProcessLanded -->|IsFalling|SetPostLandedPhysics ProcessLanded -->StartNewPhysics_2 %%--------------------- HandleImpact -.-> CalcVelocity_2 -.-> BlockingHit_2
GetFallingLateralAcceleration
:计算 Character
在水平方向上的加速度;重点是将 WorldAcceleration
通过RotateWorldToGravity
转为 Gravity
相关坐标系,然后将 Z
的方向设为 0
,再转回 World
坐标系,这样以移除垂直方向上的加速度(因为垂直方向的加速度需要由 Gravity
决定,而不是 InputVector
);
RestorePreAdditiveRootMotionVelocity
:Apply AdditiveRootMotion
的情况下将 Velocity
设置为 LastPreAdditiveVelocity
( AdditiveRootMotion
表示 RootMotioinVelocity
将与 Character
的原始速度 LastPreAdditiveVelocity
,即计算 RootMotionVelocity
前的速度叠加),防止 RootMotion Velocity
被累加;
CalcVelocity
:根据 FallAcceleration
、Gravity
、JumpForce
等数据,计算出 NewFallVelocity
;
ApplyRootMotionToVelocity
:应用 RootMotion Velocity
,根据 HasOverrideVelocity / HasAdditiveVelocity
两种应用速度方式,计算 Velocity
;
NotifyJumpApex
:当 RotateWorldToGravity(Velocity).Z < 0
时,说明到达了 JumpApex
跳跃顶点,进行通知;
SafeMoveUpdatedComponent
:进行位移设置;
BlokingHit
:在碰到障碍物时的处理;
ProcessLanded
:判断是否 IsValidLandingPoint
,如果是,进行着陆;进行通知 Landed
与设置相关物理状态 SetPostLandedPhysics
,然后开始新的物理模拟 StartNewPhysics
;
HandleImpact
:无法着陆时,AddImpactPhysicsForces
,用于后续计算碰撞后的 Velocity
与位移;
BlockingHit_2
:碰撞移动后再次计算是否再次 BlockingHit
,如果无 Hit
,则尝试 FindFloor
,找到 Floor
则尝试着陆;如果是,说明 Character
被卡在了两个障碍物中间;检测是否 IsValidLandingPoint
是着陆点,是则 ProcessLand
;如果不是着陆点,特殊处理被卡住 bDitch
的情况(检查 OldHitImpactNormal
、Hit.ImpactNormal
是否都具有 Z
即斜坡朝上,且夹角 >90°
即斜坡朝向不同,同时 Character
的 Delta.Z
接近 0
即在垂直方向无移动),如果是,尝试增加 Velocity
与位移,摆脱被卡住的情况;
Kinetic : Other
TODO…
ReplicateMoveToServer
对于 AutonomousProxy Character
将移动同步到服务器,同时进行 Client
本地的预表现;
主要维护三种 Move
数据:
Old Move
:当前还未被DS
ACK
的Move
数据中,最早的一次Move
;New Move
:本次执行(即Client
进行Perform
)的Move
;Pending Move
:若某次New Move
还未进行同步(等待并包),将其存储在Pending Move
中,等待下次同步带上该数据;
在 CallServerMovePacked
时,打包三种 Move
同步;
首先需要了解:FNetworkPredictionData
;
FNetworkPredictionData
PredictionData_Client_Character
维护 Client
的 Move
相关数据,同时用于合并、丢弃、比较、标记更新等操作;
classDiagram class FNetworkPredictionData_Client_Character { SavedMoves : TArray~FSavedMovePtr~ FreeMoves : TArray~FSavedMovePtr~ PendingMove : FSavedMovePtr LastAckedMove : FSavedMovePtr ClientUpdateRealTime : float bUpdatePosition : uint32 ... } FNetworkPredictionData_Client_Character-->FSavedMove_Character class FSavedMove_Character { TimeStamp : float DeltaTime : float Acceleration : FVector MaxSpeed : float Start / End / Saved : Location / ReletiveLocation / Rotation / Velocity / Floor / CapsuleRadius / CapsuleHalfHeight / Base / ActorOverlapCounter ... ... }
其中:
FSavedMovePtr
是 TSharedPtr<FSavedMove_Character>
;
SavedMoves
保存 Client
执行的 Move
,在 CleintAck
后, LastAckedMove
将会被 Free
并从 SavedMoves
中移除;
PendingMove
记录 Client
最新执行的,还未 CallServer
的 Move
,每次 Push
到 SavedMoves
中,同时可能会作为 OldMove
被 Combine
;
FreeMoves
记录已经被标记 Free
的 Move
,后续释放;
classDiagram class FNetworkPredictionData_Server_Character { PendingAdjustment : FClientAdjustment } FNetworkPredictionData_Server_Character-->FClientAdjustment class FClientAdjustment { TimeStamp : float DeltaTime : float bAckGoodMove : bool New Loc / Vel / Rot / Base ... ... }
PredictionData_Server_Character
记录在 Server
上的 Move
数据,用于校验、修正等;
其中:
PendingAdjustment
维护了一系列 ClientAdjust
所需的数据;
ReplicateMoveToServer - Logic
flowchart LR ReplicateMoveToServer -->GetPredictionData_Client_Character ReplicateMoveToServer -->ClientData_UpdateTimeStampAndDeltaTime ReplicateMoveToServer -->FindImportantMove ReplicateMoveToServer -->ClientData_CreateSavedMove -.->Move_SetMoveFor -.->Move_Combine ReplicateMoveToServer -->PerformMovement -.->Move_PostUpdate -.->ClientData_SaveMove ReplicateMoveToServer -->CallServerMove -.->ClearPending
GetPredictionData_Client_Character
:获取 Client
的预测数据 ClientData : FNetworkPredictionData_Client_Character*
;
ClientData
会在各个地方被更新,比如
ReplicateMoveToServer
中:
更新物理模拟的TimeStamp
与DeltaTime
:ClientData->UpdateTimeStampAndDeltaTime
;
创建新的SavedMove
:ClientData->CreateSavedMove()
;
PerformMovement
之后更新Location
、Rotation
、Velocity
等数据:NewMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Record)
;CallServerMove / CallServerMovePacked
前更新时间:ClientData->ClientUpdateRealTime = MyWorld->GetRealTimeSeconds();
ClientAckGoodMove
:Client
收到Server
的移动确认时,更新最后的移动ClientData->LastAckedMove
:
FindImportantMove
:找到最早的未 Ack
的 ImportantMove
数据,IsImportantMove
指与上一个 Ack
的移动有差异的移动;判定是否 Important
时,会检查 CompressedFlags
(压缩了 FLAG_JumpPressed
、FLAG_WantsToCrouch
等信息)、Start/End PackedMovementMode
、Acceleration
的大小、方向差异是否超过阈值;找到 Unack ImportMove
后,存储在 OldMove
中,后续将其与新的 Move
一起 CallServerMove
,确保 Server
可以正确处理。
CreateSavedMove
:创建新的 FSavedMove
数据,也就是定义一个新的 Move
;
Move_SetMoveFor
:根据 CharacterOwner
、DeltaTime
、NewAcceleration
等数据设置 Move
基本信息;
Move_Combine
:尝试将这个新的 Move
与 待处理的移动 PendingMove
合并,如果 CanCombine
,更新 Rotation
、Position
等信息;CanCombine
会校验 TimeStamp
、RootMotion
、Acceleration
、StartVelocity
、MaxSpeed
、Jump
、CompressedFlags
、MovementMode
、StartCapsule Radius/HalfHeight
、AttachParent
、TimeDilation
、ActorOverlapCounter
这些数据;Combine
时更新 Location
、Rotation
、Velocity
、Floor
、Jump
等数据;
PerformMovement
:在本地执行移动;
Move_PostUpdate
:在 PerformMovement
更新了移动相关数据之后,设置这些状态数据到 Move
中;
ClientData_SaveMove
:将 NewMove
保存到移动列表 ClientData->SavedMoves
中;
CallServerMove
:根据角色是否正在复制移动 bSendServerMove
将新的移动发动到 Server
,根据 ShouldUsePackedMovementRPCs
决定发送的方式 CallServerMovePacked / CallServerMove
;
ClearPendingMove
:清空 PendingMove
,表示没有待处理的移动;
AutonomousProxy
从 ReplicateMoveToServer -> CallServerMovePacked
继续出发:
flowchart LR CallServerMovePacked -->ServerMovePacked_ClientSend -->|DS|ServerMovePacked_Implementation -->ServerMovePacked_ServerReceive ServerMovePacked_ServerReceive -->ServerMove_HandleMoveData ServerMove_HandleMoveData -->SetCurrentNetworkMoveData ServerMove_HandleMoveData -->ServerMove_PerformMovement ServerMove_PerformMovement -->MoveAutonomous ServerMove_PerformMovement -->ServerMoveHandleClientError -->ServerCheckClientError MoveAutonomous -->PerformMovement
通过 CallServerMovedPacked (UnreliableRPC)
将打包的 SaveMoves
数据发送到 DS
,DS
根据 Client
发送到的数据应用 Move
数据,进行SetCurrentNetworkMoveData
、ServerMove_PerformMovent
,同时在 ServerMove_PerformMovent
中 MoveAutonomous
(内部还是 PerformMovement
)与 校验数据合法性 CheckClientError
;如果数据差异过大,则 ServerData->PendingAdjustment.bAckGoodMove = false
;
Move
数据变化后,通过 Replicate
将其复制到 Client
,主要涉及的数据有:
bReplicateMovement
:标记是否要进行Move
的同步;ReplicatedMovement
:移动数据;ReplicatedBasedMovement
:Base
的移动数据;ReplicatedMovementMode
:移动模式(Walk
、Fall
等)- 其它数据:
Transform
、RootMotion
等;
flowchart LR UNetDriver::TickFlush -->UNetDriver::ServerReplicateActors -->SendClientAdjustment SendClientAdjustment -->bAckGoodMove bAckGoodMove -->|true|ServerLastClientGoodMoveAckTime -->ShouldUsePackedMovementRPCs_Good bAckGoodMove -->|false|ServerLastClientAdjustmentTime -->ShouldUsePackedMovementRPCs_NoGood ShouldUsePackedMovementRPCs_Good -->|false|ClientAckGoodMove ShouldUsePackedMovementRPCs_NoGood -->|false|ClientAdjustPosition ShouldUsePackedMovementRPCs_Good -->|true|ServerSendMoveResponse ShouldUsePackedMovementRPCs_NoGood -->|true|ServerSendMoveResponse ServerSendMoveResponse -->MoveResponsePacked_ServerSend -->ClientMoveResponsePacked -->|Client|MoveResponsePacked_ClientReceive
同步时候,也向 Client
进行 SendClientAdjust
,通知 Client
每次 NewMove
的结果;
根据 ShouldUsePackedMovemtnRPCs
决定是否需要 ServerSendMoveResponse
;
flowchart LR MoveResponsePacked_ClientReceive -->ClientHandleMoveResponse -->IsGoodMove IsGoodMove -->|true|ClientAckGoodMove_Implementation IsGoodMove -->|false|ClientAdjustPosition_Implementation -->SetbUpdatePosition_true TickComponent -->ClientUpdatePositionAfterServerUpdate
Client
收到 DS
的 SendClientAdjust
后,判定 MoveResponse
是否 IsGoodMove
,如果是,Client
进行 Ack
,被确认的 Move
将会立刻从 SavedMoves
中移除;否则 Client
需要更新 bUpdatePosition
为 true
,后续在 ClientUpdatePositionAfterServerUpdate
中进行修正;
ClientUpdatePositionAfterServerUpdate
:判定 bUpdatePosition
是否是 true
,如果是则回放 DS
未 Ack
的 ClientData->SavedMoves.Num()
,进行 SetCurrentReplayedSavedMove
并 MoveFor
;
SimulateProxy
flowchart LR ACharacter::OnRep_ReplicatedMovement -->AActor::PostNetReceiveVelocity ACharacter::OnRep_ReplicatedMovement -->ACharacter::PostNetReceiveLocationAndRotation AActor::PostNetReceiveVelocity -->UPrimitiveComponent::SetPhysicsLinearVelocity ACharacter::PostNetReceiveLocationAndRotation -->SmoothCorrection ACharacter::PostNetReceiveLocationAndRotation -->SetbNetworkUpdateReceived_true
数据同步后,Client
通过 OnRep_ReplicatedBasedMovement
、OnRep_ReplicatedMovement
将坐标设置给 Actor
;
flowchart LR TickComponent -->SimulatedTick -->SimulateMovement SimulatedTick -->|!bNetworkSmoothingComplete|SmoothClientPosition %% ----------- SimulateMovement -->ScopedUpdates ScopedUpdates -->bIsSimulatedProxy bIsSimulatedProxy -->bNetworkUpdateReceived_true bNetworkUpdateReceived_true -->|bNetworkGravityDirectionChanged|SetGravityDirection bNetworkUpdateReceived_true -->|bNetworkMovementModeChanged| ApplyNetworkMovementMode bNetworkUpdateReceived_true -->|bJustTeleported OR bForceNextFloorCheck|UpdateFloorFromAdjustment bNetworkUpdateReceived_false -->|bForceNextFloorCheck|UpdateFloorFromAdjustment %% ----------- ScopedUpdates -->UpdateCharacterStateBeforeMovement ScopedUpdates -->MaybeUpdateBasedMovement ScopedUpdates -->UpdateProxyAcceleration ScopedUpdates -->|!bHandledNetUpdate OR !bNetworkSkipProxyPredictionOnNetUpdate|MoveSmooth -->IsMovingOnGround IsMovingOnGround -->|true|MoveAlongFloor IsMovingOnGround -->|false|SafeMoveUpdatedComponent -->|!bSteppedUp|SlideAlongSurface ScopedUpdates -->UpdateCharacterStateAfterMovement ScopedUpdates -->OnMovementUpdated SimulateMovement -->CallMovementUpdateDelegate SimulateMovement -->UpdateComponentVelocity %% ----------- SmoothClientPosition -->SmoothClientPosition_Interpolate SmoothClientPosition -->SmoothClientPosition_UpdateVisuals
Smooth
:
SmoothingServerTimeStamp
表示 Character
在 DS
当前移动时间戳,由 ACharacter::PreReplication
时,同步的 ReplicatedServerLastTransformUpdateTimeStamp
得来;
SmoothingClientTimeStamp
表示 Character
在这个 Client
当前平滑到的移动时间戳;
每次进行 SmoothClientPosition_Interpolate
时;
在 SmoothingMode = ENetworkSmoothingMode::Linear
的情况下:
- 计算
TargetDelta = LastCorrectionDelta
,这里的LastCorrectionDelta = 上一次( SmoothingServerTimeStamp - SmoothingClientTimeStamp)
,表示实际上相比于DS
上的数据,Client
在这一次平滑开始前,还剩余多少时间还未执行平滑操作; - 更新
SmoothingClientTimeStamp = Min(SmoothingClientTimeStamp + DeltaSeconds, SmoothingServerTimeStamp + MaxTimeAhead);
这里的DeltaSeconds
表示当帧过去的实际时间;
MaxTimeAhead = TargetDelta * 0.15f
,表示允许多往前外插的时间,0.15f
是允许多预测的时间比例;
现在这个新的SmoothingClientTimeStamp
,就表示这一帧Client
需要平滑到的时间戳; - 计算
RemainingTime = SmoothingServerTimeStamp - SmoothingClientTimeStamp
, 表示在这一帧平滑过后,还剩下多少时间没有平滑;然后CurrentSmoothTime = TargetDelta - RemainingTime
,得到这一帧需要平滑多少时间; - 计算
LerpPercent = FMath::Clamp(CurrentSmoothTime / TargetDelta, 0.0f, LerpLimit)
,按照本次平滑多少时间 / 剩余的总共需要平滑的时间
,得到这个LerpPercent
;
其中LerpLimit = 1.15f
,也是允许多平滑的比例; - 得到
LerpPercent
后,更新MeshTranslationOffset
、MeshRotationOffset
;
特别地,如果不是通过 DS
模式来进行同步(比如自定义协议),可以打包 FRepMovement
数据,然后手动进行 Replicate
模拟:
1 | CharacterActor->PreNetReceive(); |
参考
UE 5.4 源码
大体框架:UE4 移动的网络同步